// Copyright 2022 Huawei Cloud Computing Technology Co., Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <cstdio>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include "libs/libopus/include/opus.h"
#include "CasLog.h"
#include "CasOpusBridge.h"

const int SAMPLE_RATE = 48000;
const int CHANNELS = 2;
const int BITRATE = SAMPLE_RATE * 4;
const int APPLICATION = OPUS_APPLICATION_AUDIO;
const int FRAME_SIZE = SAMPLE_RATE / 100;
const int MAX_FRAME_SIZE = 6 * FRAME_SIZE;
const int MAX_PACKET_SIZE = 3 * 1276;
const int SHORT_SIZE = 16;

OpusEncoder *g_encoder = nullptr;

extern "C" JNIEXPORT jstring JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_stringFromJNI(JNIEnv *env,
    jclass type)
{
    std::string hello = "Hello from opus_jni.cpp";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jlong JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_createOpusDecoder(JNIEnv *env,
    jclass type)
{
    int err = 0;
    OpusDecoder *decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &err);
    if (err < 0) {
        ERR("Failed to create decoder %s", opus_strerror(err));
        return (jlong)(JNI_ERR);
    }
    return reinterpret_cast<jlong>(decoder);
}

extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_destroyOpusDecoder(JNIEnv *env,
    jclass type, jlong decoder)
{
    OpusDecoder *decoderPtr = reinterpret_cast<OpusDecoder *>(decoder);
    if (decoderPtr != nullptr) {
        opus_decoder_destroy(decoderPtr);
    }

    return JNI_OK;
}

extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_opusDecode(JNIEnv *env,
    jclass type, jlong decoder, jbyteArray inputBuffer, jint inputBufferLen, jshortArray outputBuffer, jint outbufLen)
{
    OpusDecoder *decoderPtr = reinterpret_cast<OpusDecoder *>(decoder);
    jbyte *inputData = env->GetByteArrayElements(inputBuffer, nullptr);
    jshort *outputData = env->GetShortArrayElements(outputBuffer, nullptr);

    if (inputData == nullptr) {
        ERR("Input data is null.");
        return JNI_ERR;
    }

    if (outputData == nullptr) {
        ERR("Output data is null.");
        return JNI_ERR;
    }

    jbyte *decodeInputData = inputData;
    jshort *decodeOutputData = outputData;

    int outLen = 0;
    while (inputBufferLen > 0) {
        int bytes = inputBufferLen;
        int decodeLen =
            opus_decode(decoderPtr, (unsigned char *)decodeInputData, bytes, (opus_int16 *)decodeOutputData, FRAME_SIZE, 0);
        if (decodeLen != FRAME_SIZE) {
            ERR("Opus decode error decoder len %d.", decodeLen);
            return JNI_ERR;
        }
        outLen += FRAME_SIZE * CHANNELS;
        decodeInputData += bytes + SHORT_SIZE;
        decodeOutputData += FRAME_SIZE * CHANNELS;
        inputBufferLen -= bytes + SHORT_SIZE;
    }

    env->ReleaseByteArrayElements(inputBuffer, (jbyte *)inputData, JNI_ABORT);
    env->ReleaseShortArrayElements(outputBuffer, (jshort *)outputData, JNI_ABORT);

    return (jint)(outLen);
}

extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_createOpusEncoder(JNIEnv *env,
    jclass type, jint sampleRate, jint channels)
{
    int err = 0;
    g_encoder = opus_encoder_create(sampleRate, channels, APPLICATION, &err);
    if (err < 0) {
        ERR("Failed to create an encoder %s", opus_strerror(err));
        g_encoder = nullptr;
        return JNI_ERR;
    }
    err += opus_encoder_ctl(g_encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));
    err += opus_encoder_ctl(g_encoder, OPUS_SET_VBR(1));
    err += opus_encoder_ctl(g_encoder, OPUS_SET_COMPLEXITY(10));
    if (err < 0) {
        ERR("Failed to set bitrate %s", opus_strerror(err));
        g_encoder = nullptr;
        return JNI_ERR;
    }

    return JNI_OK;
}

extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_destroyOpusEncoder(JNIEnv *env,
    jclass type)
{
    if (g_encoder != nullptr) {
        opus_encoder_destroy(g_encoder);
    }
    g_encoder = nullptr;

    return JNI_OK;
}

extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_opusEncode(JNIEnv *env,
    jclass type, jshortArray inputBuffer, jint frameSize, jbyteArray outBuffer)
{
    jshort *inputData = env->GetShortArrayElements(inputBuffer, 0);
    jbyte *outputData = env->GetByteArrayElements(outBuffer, 0);

    if (inputData == nullptr) {
        ERR("Input data is null.");
        env->ReleaseShortArrayElements(inputBuffer, inputData, 0);
        env->ReleaseByteArrayElements(outBuffer, outputData, 0);
        return JNI_ERR;
    }

    if (outputData == nullptr) {
        ERR("Output data is null.");
        env->ReleaseShortArrayElements(inputBuffer, inputData, 0);
        env->ReleaseByteArrayElements(outBuffer, outputData, 0);
        return JNI_ERR;
    }

    int compressedSize =
        opus_encode(g_encoder, (opus_int16 *)inputData, frameSize, (unsigned char *)outputData, MAX_PACKET_SIZE);

    if (compressedSize <= 0) {
        ERR("Opus encode error.");
        env->ReleaseShortArrayElements(inputBuffer, inputData, 0);
        env->ReleaseByteArrayElements(outBuffer, outputData, 0);
        return JNI_ERR;
    }

    env->ReleaseShortArrayElements(inputBuffer, inputData, 0);
    env->ReleaseByteArrayElements(outBuffer, outputData, 0);
    return (jint)(compressedSize);
}

extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_getOpusEncoderStatus(JNIEnv *env,
    jclass type)
{
    if (g_encoder == nullptr) {
        return JNI_ERR;
    } else {
        return JNI_OK;
    }
}
